﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Security.Cryptography;
using System.Text;

namespace COM.Service
{
    public interface ISaltHashService
    {
        void GetHashWithSaltString(string Data, out string Hash);
        bool VerifyHashWithSaltData(byte[] plainData, byte[] hashWithSaltData);
        bool VerifyHashWithSaltString(string plainText, string hashWithSaltText);
    }

    public class SaltHashService : ISaltHashService
    {
        HashAlgorithm HashProvider;
        int SalthLength;

        /// <summary>
        /// The constructor takes a HashAlgorithm as a parameter.
        /// </summary>
        /// <param name="HashAlgorithm">
        /// A <see cref="HashAlgorithm"/> HashAlgorihm which is derived from HashAlgorithm. C# provides
        /// the following classes: SHA1Managed,SHA256Managed, SHA384Managed, SHA512Managed and MD5CryptoServiceProvider
        /// </param>

        /// <summary>
        /// Default constructor which initialises the SaltHash with the SHA256Managed algorithm
        /// and a Salt of 4 bytes ( or 4*8 = 32 bits)
        /// </summary>
        public SaltHashService()
        {
            this.HashProvider = new SHA256Managed();
            this.SalthLength = 4;
            //生成的Hash码32位,再加4位盐,共36位
        }

        /// <summary>
        /// The actual hash calculation is shared by both GetHashAndSalt and the VerifyHash functions
        /// </summary>
        /// <param name="Data">A byte array of the Data to Hash</param>
        /// <param name="Salt">A byte array of the Salt to add to the Hash</param>
        /// <returns>A byte array with the calculated hash</returns>

        private byte[] ComputeHash(byte[] Data, byte[] Salt)
        {
            // Allocate memory to store both the Data and Salt together
            byte[] DataAndSalt = new byte[Data.Length + SalthLength];

            // Copy both the data and salt into the new array
            Array.Copy(Data, DataAndSalt, Data.Length);
            Array.Copy(Salt, 0, DataAndSalt, Data.Length, SalthLength);

            // Calculate the hash
            // Compute hash value of our plain text with appended salt.
            return HashProvider.ComputeHash(DataAndSalt);
        }

        /// <summary>
        /// Given a data block this routine returns both a Hash and a Salt
        /// </summary>
        /// <param name="Data">
        /// A <see cref="System.Byte"/>byte array containing the data from which to derive the salt
        /// </param>
        /// <param name="Hash">
        /// A <see cref="System.Byte"/>byte array which will contain the hash calculated
        /// </param>
        /// <param name="Salt">
        /// A <see cref="System.Byte"/>byte array which will contain the salt generated
        /// </param>
        private void GetHashAndSalt(byte[] Data, out byte[] Hash, out byte[] Salt)
        {
            // Allocate memory for the salt
            Salt = new byte[SalthLength];
            // Strong runtime pseudo-random number generator, on Windows uses CryptAPI
            // on Unix /dev/urandom
            RNGCryptoServiceProvider random = new RNGCryptoServiceProvider();
            // Create a random salt
            random.GetNonZeroBytes(Salt);
            // Compute hash value of our data with the salt.
            Hash = ComputeHash(Data, Salt);
        }


        private void GetHashWithSalt(byte[] Data, out byte[] HashWithSalt)
        {
            byte[] HashOut;
            byte[] SaltOut;
            // Obtain the Hash and Salt for the given string
            GetHashAndSalt(Data, out HashOut, out SaltOut);
            HashWithSalt = new byte[HashOut.Length + SaltOut.Length];
            Buffer.BlockCopy(HashOut, 0, HashWithSalt, 0, HashOut.Length);
            Buffer.BlockCopy(SaltOut, 0, HashWithSalt, HashOut.Length, SaltOut.Length);
        }
        /// <summary>
        /// The routine provides a wrapper around the GetHashAndSalt function providing conversion
        /// from the required byte arrays to strings. Both the Hash and Salt are returned as Base-64 encoded strings.
        /// </summary>
        /// <param name="Data">
        /// A <see cref="System.String"/> string containing the data to hash
        /// </param>
        /// <param name="Hash">
        /// A <see cref="System.String"/> base64 encoded string containing the generated hash
        /// </param>
        /// <param name="Salt">
        /// A <see cref="System.String"/> base64 encoded string containing the generated salt
        /// </param>
        private void GetHashAndSaltString(string Data, out string Hash, out string Salt)
        {
            byte[] HashOut;
            byte[] SaltOut;
            // Obtain the Hash and Salt for the given string
            GetHashAndSalt(Encoding.UTF8.GetBytes(Data), out HashOut, out SaltOut);
            // Transform the byte[] to Base-64 encoded strings
            Hash = Convert.ToBase64String(HashOut);
            Salt = Convert.ToBase64String(SaltOut);
        }

        /// <summary>
        /// 把盐后缀到加盐后生成的Hash值的后面
        /// </summary>
        /// <param name="Data"></param>
        /// <param name="Hash"></param>
        public void GetHashWithSaltString(string Data, out string Hash)
        {
            byte[] HashOut;
            GetHashWithSalt(Encoding.UTF8.GetBytes(Data), out HashOut);
            Hash = Convert.ToBase64String(HashOut);
        }

        /// <summary>
        /// This routine verifies whether the data generates the same hash as we had stored previously
        /// </summary>
        /// <param name="Data">The data to verify </param>
        /// <param name="Hash">The hash we had stored previously</param>
        /// <param name="Salt">The salt we had stored previously</param>
        /// <returns>True on a succesfull match</returns>
        private bool VerifyHash(byte[] Data, byte[] Hash, byte[] Salt)
        {
            byte[] NewHash = ComputeHash(Data, Salt);
            //  No easy array comparison in C# -- we do the legwork
            if (NewHash.Length != Hash.Length)
                return false;

            for (int Lp = 0; Lp < Hash.Length; Lp++)
                if (!Hash[Lp].Equals(NewHash[Lp]))
                    return false;

            return true;
        }

        /// <summary>
        /// This routine provides a wrapper around VerifyHash converting the strings containing the
        /// data, hash and salt into byte arrays before calling VerifyHash.
        /// </summary>
        /// <param name="Data">A UTF-8 encoded string containing the data to verify</param>
        /// <param name="Hash">A base-64 encoded string containing the previously stored hash</param>
        /// <param name="Salt">A base-64 encoded string containing the previously stored salt</param>
        /// <returns></returns>
        private bool VerifyHashString(string Data, string Hash, string Salt)
        {
            byte[] HashToVerify = Convert.FromBase64String(Hash);
            byte[] SaltToVerify = Convert.FromBase64String(Salt);
            byte[] DataToVerify = Encoding.UTF8.GetBytes(Data);
            return VerifyHash(DataToVerify, HashToVerify, SaltToVerify);
        }

        /// <summary>
        /// 把盐从Hash值的后缀中分离出来
        /// </summary>
        /// <param name="hashedAndSaltText"></param>
        /// <param name="hashedtext"></param>
        /// <param name="salt"></param>
        private void ExtractSalt(byte[] hashedAndSaltText,out byte[] hashedtext,out byte[] salt)
        {
            if (hashedAndSaltText == null)
                throw new ArgumentNullException("hashedtext");

            if (hashedAndSaltText.Length <= SalthLength)
                throw new ArgumentNullException("Hash值中没有包含Salt值");

            hashedtext = new byte[hashedAndSaltText.Length - SalthLength];
            Buffer.BlockCopy(hashedAndSaltText, 0, hashedtext, 0, hashedtext.Length);

            salt = new byte[SalthLength];
            Buffer.BlockCopy(hashedAndSaltText, hashedtext.Length, salt, 0, salt.Length);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="plaintext">明文</param>
        /// <param name="hashedtext">hashedtext后缀salt字符</param>
        /// <returns></returns>
        public bool VerifyHashWithSaltData(byte[] plainData, byte[] hashWithSaltData)
        {
            if (plainData == null) 
                throw new ArgumentNullException("plainText");
            if (plainData == null) 
                throw new ArgumentNullException("hashedText");
            if (plainData.Length == 0) throw 
                new ArgumentException("hashedText.Length");

            byte[] salt;
            byte[] hash;
            this.ExtractSalt(hashWithSaltData, out hash, out salt);
            return VerifyHash(plainData, hash, salt);
        }

        /// <summary>
        /// 验证明文和Hash值,这个Hash值后缀了盐,分离出来后,再附到明文后生成Hash值进行比对
        /// </summary>
        /// <param name="plainText"></param>
        /// <param name="hashWithSaltText"></param>
        /// <returns></returns>
        public bool VerifyHashWithSaltString(string plainText, string hashWithSaltText)
        {
            byte[] plainData = Encoding.UTF8.GetBytes(plainText);
            byte[] hashWithSaltData = Convert.FromBase64String(hashWithSaltText);
            return VerifyHashWithSaltData(plainData, hashWithSaltData);
        }

    }

    /// <summary>
    /// This little demo code shows how to encode a users password.
    /// </summary>
    public class SaltHashDemo
    {
        public void Test()
        {
            // We use the default SHA-256 & 4 byte length
            SaltHashService demo = new SaltHashService();

            // We have a password, which will generate a Hash and Salt
            string Password = "MyGlook234";
            string Hash;

            byte[] v = Encoding.UTF8.GetBytes("123456");
            var s = new SHA256Managed().ComputeHash(v);
            //s = 141+150+158...+230+134+...+220+108+146

            //demo.GetHashAndSaltString(Password, out Hash, out Salt);
            demo.GetHashWithSaltString(Password, out Hash);
            Console.WriteLine("Password = {0} , Hash = {1} , Salt = {2}", Password, Hash, "");

            // Password validation
            //
            // We need to pass both the earlier calculated Hash and Salt (we need to store this somewhere safe between sessions)

            // First check if a wrong password passes
            string WrongPassword = "OopsOops";
            //Console.WriteLine("Verifying {0} = {1}", WrongPassword, demo.VerifyHashString(WrongPassword, Hash, Salt));
            Console.WriteLine("Verifying {0} = {1}", WrongPassword, demo.VerifyHashWithSaltString(WrongPassword, Hash));

            // Check if the correct password passes
            //Console.WriteLine("Verifying {0} = {1}", Password, demo.VerifyHashString(Password, Hash, Salt));
            Console.WriteLine("Verifying {0} = {1}", Password, demo.VerifyHashWithSaltString(Password, Hash));
        }
    }
}